先前我們介紹了 Logging 與 Metrics 兩個服務監控機制,今天要來探討 Moleculer 內建的 Tracing 模組,它用於收集 Moleculer 內部應用的追蹤資訊,你可以使用 Tracing 工具來建立服務間的相互關係圖[Fig. 1.] 。內建有 Zipkin 、 Jaeger 、 Datadog 可以直接使用。
注意,此功能可能會在 v0.15 版本進行大幅改動,你可能需要考慮是否要使用目前內建的模組,詳情請參考上一篇文章。

Fig. 1. Grafana Tempo 的 Node Graph[2]
範例:快速使用
moleculer.config.js
// moleculer.config.js
module.exports = {
    tracing: true
};
範例:選項設定方式
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
enabled | 
<Boolean> | false | 
啟動 tracing 功能 | 
exporter | 
<Object> | <Object[]> | null | 
Tracing 輸出配置 | 
sampling | 
<Object> | 取樣設定 | |
actions | 
<Boolean> | true | 
在 Actions 啟用 Tracing | 
events | 
<Boolean> | false | 
在事件啟用 Tracing | 
errorFields | 
<String[]> | ["name", "message", "code", "type", "data"] | 
要加到 Span 標籤的錯誤物件欄位 | 
stackTrace | 
<Boolean> | false | 
是否要將錯誤案件的堆疊追蹤資訊加到 Span 標籤 | 
tags | 
<Object> | null | 
加入客製化 Span 標籤到 Actions 與事件 | 
defaultTags | 
<Object> | null | 
預設標籤,它會被加進所有的 spans 。 | 
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        exporter: "Console",
        events: true,
        stackTrace: true
    }
};
Moleculer 有多種 Tracing 的取樣方法。它會由 Root Span 開始確認是否有正常取樣且傳播到所有的 Child Spans 。如此一來,可以確保無論使用哪種取樣方法或速率,都會輸出完整追蹤路線。
此取樣方法使用 0 ~ 1 的固定取樣率,設為 1 表示所有的 Spans 都將被取樣,設為 0 時則都不取樣。
範例:取樣所有 Spans
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            rate: 1.0
        }
    }
};
範例:取一半的 Spans
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            rate: 0.5
        }
    }
};
此取樣方法採用速率限制,你可以設置一秒鐘內可以取樣幾個 Spans 。
範例:每秒取樣 2 個 Spans
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            tracesPerSecond: 2
        }
    }
};
範例:每 10 秒取樣 1 個 Span
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        sampling: {
            tracesPerSecond: 0.1
        }
    }
};
Tracing 支援多個輸出器、客製化 tracing spans 及整合儀表板套件 (例如: dd-trace[3] )。
Console 是一種除錯用輸出器,可以將完整的本地 trace 列印到主控台。但是只能本地使用,不能遠端追蹤遠端呼叫。
範例:
module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Console",
            options: {
                // 客製化 logger
                logger: null,
                // 啟用顏色
                colors: true,
                // 寬度
                width: 100,
                // 測量寬度
                gaugeWidth: 40
            }
        }
    }
};
Datadog 輸出器會透過 dd-trace[3] 將 tracing 資料發送到 Datadog 伺服器。
使用前請安裝 dd-trace 套件
npm install dd-trace --save。

Fig. 2. Datadog
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Datadog",
            options: {
                // Datadog 服務器 URL
                agentUrl: process.env.DD_AGENT_URL || "http://localhost:8126",
                // 環境變數
                env: process.env.DD_ENVIRONMENT || null,
                // 取樣優先度,更多請參閱[4]
                samplingPriority: "AUTO_KEEP",
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null,
                // 客製化 Datadog Tracer 選項,更多請參閱[5]
                tracerOptions: null,
            }
        }
    }
};
moleculer.config.js
Event 輸出器會將 tracing 資料發送到 Moleculer 事件 ( $tracing.spans ) 。
module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Event",
            options: {
                // 事件名稱
                eventName: "$tracing.spans",
                // 當 span 開始時發送事件
                sendStartSpan: false,
                // 當 span 結束後發送事件
                sendFinishSpan: true,
                // 是否廣播
                broadcast: false,
                // 事件群組
                groups: null,
                // 發送時間間隔 (秒)
                interval: 5,
                // 發送前的客製化 Span 物件轉換器
                spanConverter: null,
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};
為了避免與舊版混淆,本文不特別解說舊版的事件輸出器,會來看本系列的人應該也不會是舊版使用者,欲知詳情可以查看官方手冊說明:
https://moleculer.services/docs/0.14/tracing.html#Event-legacy
Jaeger 輸出器會發送 tracing spans 資訊到 Jaeger 伺服器。
使用前請安裝 jaeger-client 套件
npm install jaeger-client --save。

moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Jaeger",
            options: {
                // HTTP 報表產生器端點
                endpoint: null,
                // UDP 發送器 host
                host: "127.0.0.1",
                // UDP 發送器連接埠
                port: 6832,
                // Jaeger 取樣配置
                sampler: {
                    // 取樣類型[6] 
                    type: "Const",
                    // 取樣配置
                    options: {}
                },
                // `Jaeger.Tracer` 的附加選項
                tracerOptions: {},
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};
Zipkin 輸出器會發送 tracing spans 資訊到 Zipkin 伺服器。

moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        exporter: {
            type: "Zipkin",
            options: {
                // Zipkin 伺服器的 Base URL
                baseURL: "http://localhost:9411",
                // 發送時間間隔 (秒)
                interval: 5,
                // 附加資料選項
                payloadOptions: {
                    // 設定啟用 `debug` 屬性
                    debug: false,
                    // 設定啟用 `shared` 屬性
                    shared: false
                },
                // 預設標籤,它會被加進所有的 spans
                defaultTags: null
            }
        }
    }
};
NewRelic 輸出器會以 Zipkin v2 格式發送 tracing spans 資訊到 NewRelic 伺服器。
moleculer.config.js
{
    tracing: {
        enabled: true,
        events: true,
        exporter: [
            {
                type: 'NewRelic',
                options: {
                // NewRelic 伺服器的 Base URL
                    baseURL: 'https://trace-api.newrelic.com',
                    // NewRelic Insert Key
                    insertKey: 'my-secret-key',
                    // 發送時間間隔 (秒)
                    interval: 5,
                    // 附加資料選項
                    payloadOptions: {
                        // 設定啟用 `debug` 屬性
                        debug: false,
                        // 設定啟用 `shared` 屬性
                        shared: false,
                    },
                    // 預設標籤,它會被加進所有的 spans
                    defaultTags: null,
                },
            },
        ],
    },    
}
你也可以建立客製化的輸出器,官方建議可以參考 Console Exporter[9] 的原始碼來修改,再實作 init 、 stop 、 spanStarted 、 spanFinished 方法。
範例:建立客製化 Tracing
my-tracing-exporter.js
const TracerBase = require("moleculer").TracerExporters.Base;
class MyTracingExporters extends TracerBase {
    init() { /*...*/ }
    stop() { /*...*/ }
    spanStarted() { /*...*/ }
    spanFinished() { /*...*/ }
}
module.exports = MyTracingExporters;
範例:使用客製化 Tracing
moleculer.config.js
const MyTracingExporters = require("./my-tracing-exporter");
module.exports = {
    tracing: {
        enabled: true,
        exporter: [
            new MyTracingExporters(),
        ]
    }
};
你可以在 exporter 以陣列來定義多個輸出器。
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        exporter: [
            "Console",
            {
                type: "Zipkin",
                options: {
                    baseURL: "http://localhost:9411",
                }
            },
            {
                type: "Jaeger",
                options: {
                    host: "127.0.0.1",
                }
            }
        ]
    }
};
你可以在 Actions 或事件處理器裡面使用 ctx.startSpan 及 ctx.finishSpan 方法來加入新的 spans 。
posts.service.js
module.exports = {
    name: "posts",
    actions: {
        async find(ctx) {
            const span1 = ctx.startSpan("get data from DB", {
                tags: {
                    ...ctx.params
                }
            }); 
            const data = await this.getDataFromDB(ctx.params);
            ctx.finishSpan(span1);
            const span2 = ctx.startSpan("populating");
            const res = await this.populate(data);
            ctx.finishSpan(span2);
            return res;
        }
    }
};
範例:當 Context 不可用時,你也可以使用 broker.tracer 來建立 Span 。
posts.service.js
module.exports = {
    name: "posts",
    started() {
        // 建立一個 span 來初始化資料庫
        const span = this.broker.tracer.startSpan("initializing db", {
            tags: {
                dbHost: this.settings.dbHost
            }
        });
        await this.db.connect(this.settings.dbHost);
        // 建立 sub-span 來建立資料表
        const span2 = span.startSpan("create tables");
        await this.createDatabaseTables();
        // 結束 sub-span
        span2.finish();
        // 結束 span
        span.finish();
    }
};
當你使用外部隊列通訊時也可以連接到 Spans (例如: moleculer-channels[10] )。你只需要將 parentID 及 requestID 丟給處理程序,然後使用這些 ID 來啟動客製化 span。
範例:連接 Spans
module.exports = {
	name: "trace",
	actions: {
		async extractTraces(ctx) {
			// 從 Context 取得 `parentID` 及 `requestID`
			const { parentID, requestID: traceID } = ctx;
			// 將 `parentID` 及 `traceID` 作為參數發送到遠端隊列
			await this.broker.sendToChannel("trace.setSpanID", {
				// 發送 IDs 參數
				parentID,
				traceID,
			});
		},
	},
	channels: {
		"trace.setSpanID"(payload) {
			// 將參數帶進客製化 span
			const span = this.broker.tracer.startSpan("my.span", payload);
			// ... 撰寫邏輯
			span.finish(); // 結束客製化 span
		},
	},
};
你可以客製化 trace span 的名稱。這種情況下,必須將 spanName 設為一個 String 或是 Function 。
範例:建立客製化名稱函數
posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                spanName: ctx => `Get a post by ID: ${ctx.params.id}`
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};
你可以設定要將那些 Context 的 params 或 meta 加進 span 標籤。
範例:預設只會加入 ctx.params 的所有屬性
posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // 加入 `params` ,但不加入 `meta`
                tags: {
                    params: true,
                    meta: false,
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};
範例:客製化 params
posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                tags: {
                    // 加入 `ctx.params.id`
                    params: ["id"],
                    // 加入 `ctx.meta.loggedIn.username`
                    meta: ["loggedIn.username"],
                    // 在 action 響應中加入標籤
                    response: ["id", "title"]
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};
範例:使用客製化函數來產生 span 標籤
posts.service.js
module.exports = {
    name: "posts",
    actions: {
        get: {
            tracing: {
                // 可由引數取得 Context 資料
                tags(ctx, response) {
                    return {
                        params: ctx.params,
                        meta: ctx.meta,
                        custom: {
                            a: 5
                        },
                        response
                    };
                }
            },
            async handler(ctx) {
                // ...
            }
        }
    }
};
注意,當你在 Action 中使用這個方法時,執行成功的話函數將會被呼叫兩次。第一次只會響應
ctx資訊,第二次則會包含ctx與response資訊。
你可以在配置的 tracing.tags 中設定全域的 Action 與事件的 span 標籤,除非你在服務的 Action 與事件內去覆蓋設定,否則它將會套用到所有的 Action 與事件。所有前述的客製化標籤類型都是有效的。雖然在服務中 Action 與事件的標籤都是優先套用的,但 params 、 meta 與 response 標籤的定義卻不是,意思就是你仍然可以在每個服務中,去定義全域的 meta 標籤或本地 response 標籤。
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        tags: {
            action: {
                // 永不加入 params
                params: false,
                // 由 `ctx.meta` 新增 `loggedIn.username` 值
                meta: ["loggedIn.username"],
                // 總是加入 response
                response: true,
            },
            event(ctx) {
                return {
                    params: ctx.params,
                    meta: ctx.meta,
                    // 加入呼叫功能
                    caller: ctx.caller,
                    custom: {
                        a: 5
                    },
                };
            },
        }
    }
};
使用配置的 tags 屬性所定義的客製化標籤可以使用
ctx引數,如果是在 action 使用還可以拿到response。若是在defaultTags選項中定義的標籤則必須是靜態的物件,或是寫成一個函數並在接收 tracer 實例後返回一個物件。它也可以透過 tracer 實例請求 broker 實例,但無法透過ctx請求。
範例:事件 tracing 只需在設置中加上 events: true 。
moleculer.config.js
module.exports = {
    tracing: {
        enabled: true,
        events: true
    }
};
通常不建議在 ctx.params 或 ctx.meta 發送不可序列化的參數(例如: http request 、 socket 實例及串流實例等)。如果啟用了 tracing ,則 tracer 輸出器將試著遞迴展平參數(使用 flattenTags method[11] ),這將會導致最大呼叫堆疊錯誤。
為了避免發生問題,可以在輸出器設定使用 safetyTags ,設為 true 的時候,輸出器將不會進入循環展平,此選項可以在任何的內建輸出器使用。
注意,當你啟動這個功能,有可能導致系統的效率下降。
範例:全域使用 safetyTags
moleculer.config.js
{
    tracing: {
        exporter: [{
            type: "Zipkin",
            options: {
                safetyTags: true,
                baseURL: "http://127.0.0.1:9411"
            }
        }]
    }
}
為避免影響所有的 Action ,你可以只在 Action 啟用 safetyTags ,以避免其它 Action 受到影響。
broker.createService({
    name: "greeter",
    actions: {
        hello: {
            tracing: {
                safetyTags: true
            },
            handler(ctx) {
                return `Hello!`;
            }
        }
    }
});
[1] Tracing, https://moleculer.services/docs/0.14/tracing.html
[2] 淺談OpenTelemetry Specification - Trace, https://ithelp.ithome.com.tw/articles/10288979
[3] dd-trace, https://github.com/DataDog/dd-trace-js
[4] Sampling rules, https://docs.datadoghq.com/tracing/faq/trace_sampling_and_storage/?tab=java#sampling-rules
[5] Tracer settings, https://datadoghq.dev/dd-trace-js/#tracer-settings
[6] Client Sampling Configuration, https://www.jaegertracing.io/docs/1.14/sampling/#client-sampling-configuration
[7] Zipkin, https://zipkin.io/
[8] new relic, https://newrelic.com/
[9] Moleculer Console Exporter, https://github.com/moleculerjs/moleculer/blob/master/src/tracing/exporters/console.js
[10] moleculer-channels, https://github.com/moleculerjs/moleculer-channels
[11] flattenTags method, https://github.com/moleculerjs/moleculer/blob/c48d5a05a4f4a1656075faaabc64085ccccf7ef9/src/tracing/exporters/base.js#L87-L101